package org.checkerframework.framework.type;

import java.util.IdentityHashMap;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable;

/** Duplicates annotated types and replaces components according to a replacement map. */
public class AnnotatedTypeCopierWithReplacement {

  /**
   * Returns a copy of type after making the specified replacements.
   *
   * @param type the type that will be copied with replaced components
   * @param replacementMap a mapping of {@literal referenceToReplace => referenceOfReplacement}
   * @return a duplicate of type in which every reference that was a key in replacementMap has been
   *     replaced by its corresponding value
   */
  public static AnnotatedTypeMirror replace(
      AnnotatedTypeMirror type,
      IdentityHashMap<? extends AnnotatedTypeMirror, ? extends AnnotatedTypeMirror>
          replacementMap) {
    return new Visitor(replacementMap).visit(type);
  }

  /**
   * AnnotatedTypeCopier maintains a mapping of {@literal typeVisited => copyOfTypeVisited} When a
   * reference, typeVisited, is encountered again, it will use the recorded reference,
   * copyOfTypeVisited, instead of generating a new copy of typeVisited. Visitor pre-populates this
   * mapping so that references are replaced not by their copies but by those in the replacementMap
   * provided in the constructor.
   *
   * <p>All types NOT in the replacement map are duplicated as per AnnotatedTypeCopier.visit
   */
  protected static class Visitor extends AnnotatedTypeCopier {

    private final IdentityHashMap<? extends AnnotatedTypeMirror, ? extends AnnotatedTypeMirror>
        originalMappings;

    public Visitor(
        final IdentityHashMap<? extends AnnotatedTypeMirror, ? extends AnnotatedTypeMirror>
            mappings) {
      originalMappings = new IdentityHashMap<>(mappings);
    }

    @Override
    public AnnotatedTypeMirror visit(AnnotatedTypeMirror type) {
      return type.accept(this, new IdentityHashMap<>(originalMappings));
    }

    @Override
    public AnnotatedTypeMirror visitTypeVariable(
        AnnotatedTypeVariable original,
        IdentityHashMap<AnnotatedTypeMirror, AnnotatedTypeMirror> originalToCopy) {
      // AnnotatedTypeCopier will visit the type parameters of a method and copy them.
      // Without this flag, any mappings in originalToCopy would replace the type parameters.
      // However, we do not replace the type parameters in an AnnotatedExecutableType.  Also,
      // AnnotatedExecutableType.typeVarTypes is of type List<AnnotatedTypeVariable> so if the
      // mapping contained a type parameter -> (Non-type variable AnnotatedTypeMirror) then a
      // runtime exception would occur.
      if (visitingExecutableTypeParam) {
        visitingExecutableTypeParam = false;
        AnnotatedTypeVariable copy =
            (AnnotatedTypeVariable)
                AnnotatedTypeMirror.createType(
                    original.getUnderlyingType(), original.atypeFactory, original.isDeclaration());
        maybeCopyPrimaryAnnotations(original, copy);
        originalToCopy.put(original, copy);

        if (original.getUpperBoundField() != null) {
          copy.setUpperBound(visit(original.getUpperBoundField(), originalToCopy));
        }

        if (original.getLowerBoundField() != null) {
          copy.setLowerBound(visit(original.getLowerBoundField(), originalToCopy));
        }
        return copy;
      }

      return super.visitTypeVariable(original, originalToCopy);
    }
  }
}
